/*
 * Copyright 2002-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.fjsei.yewu.report;

import com.querydsl.core.BooleanBuilder;
import graphql.relay.Connection;
import graphql.schema.DataFetchingEnvironment;
import jakarta.persistence.OptimisticLockException;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import md.cm.base.Companies;
import md.cm.flow.ApprovalStm;
import md.cm.unit.Unit;
import md.cm.unit.Units;
import md.specialEqp.QReport;
import md.specialEqp.Report;
import md.specialEqp.ReportRepository;
import md.specialEqp.inspect.*;
import md.system.User;
import org.fjsei.yewu.dto.RepLink;
import org.fjsei.yewu.exception.CommonGraphQLException;
import org.fjsei.yewu.graphql.DbPageConnection;
import org.fjsei.yewu.graphql.IdMapper;
import org.fjsei.yewu.input.IspCommonInput;
import org.fjsei.yewu.jpa.PageOffsetFirst;
import org.fjsei.yewu.pojo.sei.IspTzFieldSnapshot;
import org.fjsei.yewu.resolver.Comngrql;
//import org.fjsei.yewu.bpm.ApprovalStmService;
import org.fjsei.yewu.util.Tool;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
//import org.springframework.retry.annotation.Backoff;
//import org.springframework.retry.annotation.Retryable;
import java.lang.reflect.Field;
import java.util.*;


/**默认Unit模型的属性； graphQL接口;
 * 底下批处理方案还不是一种的。 注释掉的也是1个方案。
 * 每一个graphQL的Object对象type都需单独声明一个XxxController implements IdMapper<type> {} 否则Id都无法转换成Relay要的GlobalID的。
 * */

@Slf4j
@Controller
public class ReportController extends Comngrql implements IdMapper<Report> {
	private final Units units;
	private final Companies companies;
	private final TaskRepository tasks;
	private final IspRepository isps;
	//private final ApprovalStmRepository  approvalStms; 基类已经注入该表了，就不要重复：两个实例。
	private final ReportRepository  reports;
//	private final ApprovalStmService approvalStmService;
	/** registry参数：IDEA报红却能运行成功，实际运行注入DefaultBatchLoaderRegistry;
     *  消除报红，测试平台Units没有报红 https://blog.csdn.net/zy103118/article/details/84523452
	 * 添加了 @ComponentScan({"md","org.fjsei.yewu"}) 以后: Units报红消除了。
	 * */
	public ReportController(BatchLoaderRegistry registry, Units units, Companies companies, TaskRepository tasks, IspRepository isps, ReportRepository reports) {
		this.units = units;
		this.companies = companies;
		this.tasks = tasks;
		this.isps = isps;
		this.reports = reports;
//		this.approvalStmService = approvalStmService;
	}
	//实际可直接node(GlobalID)替换的；   不能用Long id，graphQL已改成ID
	//参数不对Cannot invoke "Object.getClass()" because "source" is null， 需要@Argument()改名，或者Report report无法注入的参数。
	@QueryMapping
	public Report getReport(@Argument("id") String sId) {
		Report report = entityOf(sId,Report.class);
//		Tool.ResolvedGlobalId gId= Tool.fromGlobalId(sId);
//		Long id=Long.valueOf(gId.getId());
		//Report report = reportRepository.findById(id).orElse(null);
		//是否需要重新初始化技术参数设备基本字段呢？
		return report;
	}
	//不是graphQl的接口函数，若加上@Transactional有问题的：  没用？
	//@Transactional(noRollbackFor = ObjectOptimisticLockingFailureException.class, propagation = Propagation.REQUIRES_NEW)
	protected Report innerModifyAction(Report report, String data, Integer version) {
		Report.ReportBuilder repBld=report.toBuilder();
		repBld.data(data).version(version);		 //构造器模式做保存的才会触发@Version的互斥锁!
		Report  orRep2= repBld.build();	  //@Builder带来的问题：orRep在这一行 被修改成了非数据库对象了，只能是内存的对象数据。导致返回的version不是最新数据库能看见的！
		// 再次尝试更新
		Report newRp3=reports.save(orRep2);
		return newRp3;
	}
	/**复检 deduction 还要斟酌？复制不合格报告后再修改记录？ 【问题】若不加@Transactional这个接口函数还没有执行完毕，CRDB数据库就已经提交生效了！！
	 *【犹豫啊】本来应该加@Transactional 加上无法支持多人同时修改一个报告的，只能刷新确保version一致性。  但是Transactional不加导致了问题：他人或它设备一起改数据会对冲，业务层面？可接受。提醒用户选择？
	 * 最终还是决定：加上@Transactional； 方案更好的。 用户对刚刚修改的冲突重新查询，再做修改容易记住刚才变更数据。但是如果直接覆盖他人已经修改的数据的，估计他人或它设备修改的不一致数据毫无提示地被覆盖没掉了，不是更糟糕！。
	 *直接从Repository读取实体修改orRep.setVersion(version)没有效果，version是JPA自己控制增加的，但这个方式互斥锁没有被起作用！
	 *构造器模式Report orRep2=Report.ReportBuilder）repBld.build()，后再保存reports.save(orRep2)才会触发@Version的互斥锁功能，构造器自带Where version=:version吗。
	 *Report.snapshot存储前端无法编辑的，是后端初始化，Report.data存储包括了snapshot的全部字段信息可能被前端篡改，但是snapshot可作为快照和校验有无被原始记录编制时刻做了篡改。
	 将 ObjectOptimisticLockingFailureException 添加到 noRollbackFor 属性中。但请务必确保你了解这样做的后果，并已经为可能出现的并发问题制定了适当的解决方案。你的应用正在尝试修改已经被其他事务修改的数据。
	 这底下的catch (ObjectOptimisticLockingFailureException e) {重新保存地 }，但是它实际上没效果地！就算加noRollbackFor = ObjectOptimisticLockingFailureException.class也都被回滚！！
	 必须限制时间期限@Transactional(timeout=60)避免前端是等接口应答的，离线处理不会捕捉到异常。
	 * */
	@MutationMapping
	@Transactional(timeout=60)
	public Report modifyOriginalRecordData(@Argument String id,@Argument String data, @Argument String client,@Argument Integer version) {
		User user=checkAuth();
		if(null==user){
			throw new CommonGraphQLException("没有登录不能改", id);
		}
		//if(!emSei.isJoinedToTransaction())      emSei.joinTransaction();
		/*【关键毛病】前端提示Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [md.specialEqp.Report#5d3d62eb
		后端doInTransaction(ProceedingJoinPoint pjp, Transactional transactional)事务层面捕捉到 org.springframework.orm.ObjectOptimisticLockingFailureException:
		表明：这里直接Report orRep= entityOf(id,Report.class);提取的对象已经过期了吗？ orRep版本可能过时？
		* */
		Report orRep= entityOf(id,Report.class);
		//Report originalRecord= reports.findById(id).orElse(null);
		if(orRep == null)     throw new CommonGraphQLException("没有该原始记录", id);
		ApprovalStm stm=orRep.getStm();
		Assert.isTrue(Procedure_Enum.MAKE.equals(stm.getSta()) || Procedure_Enum.BEGIN.equals(stm.getSta()),"无效状态");
		boolean matchusr= stm.getAuthr().stream().anyMatch(man -> {
			return man.equals(user);
		});
		Assert.isTrue(matchusr || user.equals(stm.getMaster()),"检验员校核人才能改");
		Integer objVersion=version;
		if(version != orRep.getVersion()) {
			objVersion=orRep.getVersion();
			if(user.equals(orRep.getModifyBy()) && client.equals(orRep.getModifyDfp())) {
				log.debug("同一台设备发起的，覆盖");
			}else{
				if(null!=orRep.getModifyBy() || null!=orRep.getModifyDfp())		//不是还没人开始改的
					throw new CommonGraphQLException("操作数据记录已在其它设备或其他人改动，新版本是"+orRep.getVersion()+"旧版本是"+version, id);
			}
			//以下: 允许例外情况下,已是本人继续操作的，继续修改。
		}
		Report reportNow=innerModifyAction(orRep,data,objVersion);
		reportNow.setModifyBy(user);
		reportNow.setModifyDfp(client);
		//Report newRp=reports.save(reportNow);			//若@Version互斥这里就会抛出异常，事务异常是在底下的return之后才产生的。
			//boolean isRollback= TransactionAspectSupport.currentTransactionStatus().isRollbackOnly();
			//若用 jakarta.persistence.OptimisticLockException; 捕捉不到的！
			//实际上这下面逻辑 没任何用处！其实就算version改了，最终都同样 回滚的！isRollback导致的；【可能解决办法】只能另外执行一个graphQL接口函数，必须在由前端再发起地新发送一次街口动作。
		//Report freshEntity = reports.findById(orRep.getId()).orElseThrow(() -> new RuntimeException("Entity not found"));
		//return reportNow;		//构造器模式生成的对象不会提取数据库的version字段最新取值的。返回从Repository读取实体修改orRep做保存的才会获取数据库最新version字段取值。
		return reportNow;
	}


//		@Retryable(value = {OptimisticLockException.class}, backoff = @Backoff(delay = 1000))
//		public void updateEntity(MyEntity entity) {
//			myEntityRepository.save(entity);
//		}
//@Transactional
//public void updateEntity(Entity entity) {
//	try {
//		// 尝试更新实体
//		entityRepository.save(entity);
//	} catch (OptimisticLockException e) {
//		// 事务回滚（通常Spring事务管理会为你做这件事，但确保你的方法被@Transactional注解）
//
//		// 重新从数据库获取最新的实体（绕过缓存）
//		Entity freshEntity = entityRepository.findById(entity.getId()).orElseThrow(() -> new RuntimeException("Entity not found"));
//
//		// 将更新应用到新的实体上（如果适用）
//		// freshEntity.setSomeProperty(entity.getSomeProperty());
//
//		// 再次尝试更新
//		entityRepository.save(freshEntity);
//	}
//}示例中我假设 entityRepository.findById() 方法会绕过缓存并直接从数据库中检索实体。这取决于你的具体配置和Hibernate版本，因为默认行为可能会因版本而异。如果 findById 仍然使用缓存，你可能需要实现一个自定义的查询来绕过缓存。


	/**当前我的待处理的分项报告(不含正常主报告同时有涉及的情况)：
	 * 分项报告：正常应该扣除了我已经在该Isp的主报告当中的情况，若分项没有终结主报告肯定也没有终结的。
	 * 用QueryDSL做超复杂的关联关系的过滤多条件查询，的确比较容易看得懂，比起传统的人工书写SQL语句真得是强了太多！！传统编码很容易搞错逻辑。
	 * */
	@QueryMapping
	public Connection<Report> findMeSubReports(@Argument String orderBy, @Argument Boolean asc, @Argument IspCommonInput where, @Argument Integer first, @Argument String after, @Argument Integer last, @Argument String before, DataFetchingEnvironment env) {
		User user= checkAuth();
		if(user==null)   return null;
		DbPageConnection<Report> connection=new DbPageConnection(env);
		int offset=connection.getOffset();
		int limit=connection.getLimit();
		Pageable pageable;
		if (!StringUtils.hasLength(orderBy))
			pageable = PageOffsetFirst.of(offset, limit);
		else
			pageable = PageOffsetFirst.of(offset, limit, Sort.by(asc ? Sort.Direction.ASC : Sort.Direction.DESC, orderBy));
		QReport qm = QReport.report;
		BooleanBuilder builder = new BooleanBuilder();
		BooleanBuilder subRepPr = new BooleanBuilder();
		subRepPr.or(qm.isp.report.ne(qm));
		BooleanBuilder userPr = new BooleanBuilder();
		//默认：在这个位置qm.isp.report.stm就会出现null 空指针(IDEA观察到)，但可能也根本就没提示的  直接歇菜。querydsl解决办法@QueryInit("report.stm.master")多对一一对一的字段才会出现此类问题。
		userPr.or(qm.stm.master.eq(user)).or(qm.stm.authr.contains(user));
		BooleanBuilder staPr = new BooleanBuilder();
		staPr.or(qm.stm.sta.eq(Procedure_Enum.BEGIN)).or(qm.stm.sta.eq(Procedure_Enum.MAKE));
		//需要排除我已经是关联Isp的主报告当中的人员了。
		BooleanBuilder meInMainRepPr = new BooleanBuilder();
		//嵌套多层可能前端发现Cannot read field \"master\" because \"qm.isp.report.stm\" is null 后端实际出错：QueryDSL对3层以上一对一多对一字段没特别注解是空指针了。
		//【问题】多个@QueryInit会相互影响啊？ ?抵消，@QueryInit("report.stm.master")要重复测试 @QueryInit({"e3.*", "e33.e4", "e333"}), @QueryInit("e3.e4"), * , *.*;
		meInMainRepPr.or(qm.isp.report.stm.master.eq(user)).or(qm.isp.report.stm.authr.contains(user));
		//类似于SQL语句的Where的多个逻辑汇总 AND 4个条件;  排除面积大的条件放在前部率先执行。
		builder.and(userPr).and(subRepPr).and(staPr).and(meInMainRepPr.not());
		if(StringUtils.hasText(where.getServu())) {
			Unit unit=fromInputUnitGlobalID(where.getServu());
			Assert.notNull(unit,"未找到Unit:"+where.getServu());
			//不考虑关联isp.servu.搜寻，依据isp.bus.task.servu做关联，task表数据量更少，冷数据已清理掉了。
			//【QueryDsl】错Cannot read field task because qm.isp.bus is null，初始化加到报错的上两层关联Report模型当中做的; bus->Isp->Report;
			builder.and(qm.isp.bus.task.servu.id.eq(unit.getId()));
		}
		if(StringUtils.hasText(where.getCod()))
			builder.and(qm.isp.dev.cod.containsIgnoreCase(where.getCod()));
		if(StringUtils.hasText(where.getOid()))
			builder.and(qm.isp.dev.oid.containsIgnoreCase(where.getOid()));
		if(StringUtils.hasText(where.getIdent()))
			builder.and(qm.isp.bus.ident.containsIgnoreCase(where.getIdent()));
		if(StringUtils.hasText(where.getNo()))
			builder.and(qm.isp.no.containsIgnoreCase(where.getNo()));
		//【神奇了 querydsl】实际上这builder在这直接照抄findMeIsps(){}里面的竟然也可以！ 会从QIsp QDetail自动生成关联关系的。我都没定义QDetail的必要了。
		//这里直接在IDEA调试环节，查看 builder 变量的描述 就能判定代码是否正确。
		Slice<Report> rpage= (Slice<Report>)reports.findAll(builder,pageable);
		List<Report> isps=(List<Report>) rpage.toList();
		return connection.setListData(isps).get(env);
	}
	/**加个报告；分项的。 主报告 本身自带具备排他性。
	 * Isp底下挂接的 分项报告同一个modeltype类型的可以允许多个，主报告modeltype只能唯一个。
	 * 分项报告和收费项目没有必然联系；派工环节Task任务负责人自己决定是否增加哪些分项报告。
	 *  任务负责人dispatchTaskTo给校核人检验人员审核人审批人。默认的分项采取和主报告(dispatchTaskTo:TASK初始化时就有生成的主报告)主报告相一致的人员配备的。
	 *  后续的，任务负责人可以增加分项报告同时配置人员参数。
	 *  任务责任人dispatchTaskTo(taskDate: Date）这个操作应当是距离真正的检验工作日期非常接近的日子去做。
	 *  任务责任人才有权利执行newReport｛主报告或分项报告以及人员配置甚至收费项目参数设置的｝
	 */
	@MutationMapping
	@Transactional
	public Report newReport(@Argument("isp") String ispId,@Argument("master") String masterId,
							@Argument("ispMens") List<String> ispmenIds,@Argument("reviewer") String reviewerId,
							@Argument("approver") String approverId,@Argument String modeltype,@Argument Short modelversion) {
		User user=checkAuth();
		if(null==user)  return null;
//		Detail detail= entityOf(detId,Detail.class);
//		Assert.notNull(detail,"未找到Detail:"+detId);
		Isp isp =entityOf(ispId,Isp.class);
		Assert.notNull(isp,"未找到Isp");
		User verify = entityOf(masterId,User.class);
		Assert.notNull(verify,"未找到user:"+masterId);
		List<User> ispMens=  new ArrayList<>();
		ispmenIds.stream().forEach(item -> {
			User aUser = entityOf(item,User.class);
			Assert.isTrue(aUser != null,"未找到user:"+item);
			ispMens.add(aUser);
		});
		User reviewer = entityOf(reviewerId,User.class);
		Assert.isTrue(reviewer != null,"未找到user:"+reviewerId);
		User approver = entityOf(approverId,User.class);
		if(null==isp.getReport() )
			Assert.isTrue(approver != null,"未找到user:"+approverId);
		//报告编号码不是这里设置的， 应该在Detail/Isp设备加到Task的时间，就是分配Task给负责人 dispatchToLiabler 操作就定义好的。后面分项报告增加删除也要保持no不变。
		//万一删除了主报告忘记了旧的no编码？ 可以从分配Task给负责人环节再次补回新的no。
		String repNo= isp.getNo();		//"JD2020FTC00004";
		Report report = new Report(ReportType_Enum.WEB,repNo,isp);
		report.setModeltype(modeltype);
		report.setModelversion(modelversion);
		java.util.Date  now= new java.util.Date();
		//report.setUpLoadDate(new java.sql.Date(now.getTime()));
		ApprovalStm approvalStm=new ApprovalStm();
		approvalStm.setMaster(verify);		//负责启动zeebe流转
		approvalStm.setAuthr(ispMens);		//要签字的多个检验员
		approvalStm.setReviewer(reviewer);
		if(null==isp.getReport() )
			approvalStm.setApprover(approver);
		report.setStm(approvalStm);
		approvalStms.save(approvalStm);
		report.setUpLoadDate(now);
		reports.save(report);
		//若主报告=null的，默认设为率先设置为主报告，优先配置主报告，需要修改主报告的可以先删除主报告后再添加一个新报告，否则都是分项报告，主报告唯一性。
		if(null==isp.getReport() )
			isp.setReport(report);		 //默认还没有的 算主报告
		isps.save(isp);
		return report;
	}
	/**前端看到的都是主报告snapshot， 为避免混淆，后端尽量不要用！
	 * 分项报告采用和主报告同一份 snapshot 存储，若是分项报告则数据库该字段实际=null; 主报告才需要设置数据库snapshot字段。
	 * */
	@SchemaMapping(field="snapshot")
	public String  snapshot(Report report, DataFetchingEnvironment env) {
		Isp isp = report.getIsp();
		return  (null==isp.getReport()? null: isp.getReport().getSnapshot() );
	}

	/**不一定只能是Report模型所关联的java类的JPA实体模型的字段才能用，实际上可以拆除关系，允许添加无关联的属性的。
	 * 台账业务相关信息，静态化后给前端做报告的，有那些字段：本业务设备的类型使用的。
	 * 前端没声明tzFields字段的就不会运行这里的，前端强制刷新才会重新来这里获取的，前端relay也会做缓存的。
	 * */
	@SchemaMapping(field="tzFields")
	public ArrayList<String>  tzFields(Report report, DataFetchingEnvironment env) {
		Isp isp = report.getIsp();
		ArrayList<String> out=new ArrayList<>();
		// 获取类的字段 java反射获取所有字段名
		Class<?> clazz = IspTzFieldSnapshot.class;
		Field[] fields = clazz.getDeclaredFields();
		for (Field field : fields) {
			System.out.println(field.getName());
			out.add(field.getName());
		}
		return out;
	}
	@SchemaMapping(field="link")
	public RepLink link(Report report, DataFetchingEnvironment env) {
		return  report.getLink();
	}
}

